﻿using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Coding4Fun.BoxingBots.Common;
using Coding4Fun.BoxingBots.Communication;
using Coding4Fun.Kinect.KinectService.Common;
using Coding4Fun.Kinect.KinectService.Listeners;
using Coding4Fun.Kinect.KinectService.WpfClient;
using Coding4Fun.Kinect.Wpf;
using Microsoft.Kinect;
using NAudio.Wave;
using ColorImageFormat = Microsoft.Kinect.ColorImageFormat;
using ColorImageFrame = Microsoft.Kinect.ColorImageFrame;
using ConnectionEventArgs = Coding4Fun.BoxingBots.Communication.ConnectionEventArgs;
using ImageFormat = Coding4Fun.Kinect.KinectService.Common.ImageFormat;
using Settings = Coding4Fun.BoxingBots.UserConsole.Properties.Settings;
using Skeleton = Microsoft.Kinect.Skeleton;
using SkeletonFrameReadyEventArgs = Microsoft.Kinect.SkeletonFrameReadyEventArgs;
using SkeletonTrackingState = Microsoft.Kinect.SkeletonTrackingState;

namespace Coding4Fun.BoxingBots.UserConsole
{
	/// <summary>
	/// Interaction logic for GameWindow.xaml
	/// </summary>
	public partial class GameWindow : Window
	{
		private const double MoveClip = .3;

		private readonly Stopwatch _gameWatch = new Stopwatch();

		private readonly NetworkSettings _mySettings = Common.Settings.NetworkSettings[Settings.Default.MyPlayer];
		private readonly NetworkSettings _opponentSettings = Common.Settings.NetworkSettings[Settings.Default.OpponentPlayer];
		private readonly Random _random = new Random();
		private readonly double[] _wingMaximum = {.74, 1.12, 1.52};

		// WING DRAWING
		private readonly double[] _wingRadii = {231.0, 256.0, 283.0};
		private readonly double[] _wingWidth = {18.0, 18.0, 18.0};
		private Skeleton[] _allSkeletons;
		private AudioClient _audioClient;
		private AudioListener _audioListener;

		private ColorClient _colorClient;
		private ColorListener _colorListener;

		private double _damageBottomLeft;
		private double _damageBottomRight;
		private double _damageTopLeft;
		private double _damageTopRight;
		private SocketListener<MovementDescriptorPacket, GameStatePacket> _dataListener;
		private GameState _gameState = GameState.GameOver;
		private double _healthPerc = 1.0;
		private double _healthPercTarget = 1.0;
		private KinectSensor _kinect;

		private bool _lastLeft, _lastRight;
		private int _myHealth, _opponentHealth;
		private BufferedWaveProvider _provider;
		private bool _resetSkeleton;
		private ColorClient _robotColorClient;
		private bool _runGameLoop = true;
		private CalculatedSensorData _sensorData;
		private Skeleton _skeletonData;
		private SkeletonListener _skeletonListener;
		private double _tank0Power = 1.0;
		private double _tank1Power = 1.0;
		private int _trackingSkeletonId;
		private WaveOut _waveOut;
		
		private PathFigure[] _rightWings;
		private PathFigure[] _leftWings;

		public GameWindow()
		{
			InitializeComponent();

			Loaded += MainWindow_Loaded;
			Closed += MainWindow_Closed;
		}

		private void MainWindow_Loaded(object sender, RoutedEventArgs e)
		{
			_rightWings = new[] { figure_punchRightInner, figure_punchRightMiddle, figure_punchRightOuter };


			_leftWings = new[] { figure_punchLeftInner, figure_punchLeftMiddle, figure_punchLeftOuter };

			InitializeKinectAudio();
			InitializeDataListener();
			try
			{
				InitializeKinect();
			}
			finally
			{
				InitializeKinectService();

				CompositionTarget.Rendering += CompositionTarget_Rendering;

				var gameThread = new Thread(GameLoop);
				gameThread.IsBackground = true;
				gameThread.Start();
			}
		}

		private void MainWindow_Closed(object sender, EventArgs e)
		{
			_runGameLoop = false;

			if (_kinect != null)
			{
				_kinect.Stop();
			}

			if (_waveOut != null)
			{
				_waveOut.Stop();
				_waveOut.Dispose();
			}

			AudioPlayer.Shutdown();
		}

		private void Window_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.Key == Key.A && !e.IsRepeat)
				AdminPanel.Visibility = AdminPanel.Visibility == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
		}

		private void ConnectOpponent_Click(object sender, RoutedEventArgs e)
		{
			ConnectToOpponentConsole();
		}

		private void ConnectRobot_Click(object sender, RoutedEventArgs e)
		{
			ConnectToRobotKinectService();
		}

		private void InitializeKinectAudio()
		{
			if (Settings.Default.MuteKinectAudio)
			{
				MuteKinectAudio.IsEnabled = false;
			}
			else
			{
				_waveOut = new WaveOut();
				_provider = new BufferedWaveProvider(new WaveFormat(16000, 16, 1));

				_waveOut.Init(_provider);
				_waveOut.Play();
			}
		}

		private void InitializeDataListener()
		{
			_dataListener = new SocketListener<MovementDescriptorPacket, GameStatePacket>();

			_dataListener.ConnectionCompleted += DataListenerConnectionCompleted;
			_dataListener.PacketReceived += DataListenerPacketReceived;

			_dataListener.Start(_mySettings.DataPort);
		}

		private void DataListenerConnectionCompleted(object sender, ConnectionEventArgs e)
		{
			Debug.WriteLine("Connection to UserConsole from Scoreboard: " + e.Connected);
		}

		private void DataListenerPacketReceived(object sender, PacketReceivedEventArgs<GameStatePacket> e)
		{
			Debug.WriteLine("UserConsole: " + e.Packet);

			if (Settings.Default.MyPlayer == Player.One)
			{
				// if our new health is less than our current health, assume a hit occurred
				if (e.Packet.Robot1Health < _myHealth)
					AudioPlayer.PlayCue(SoundCue.Hit);

				_myHealth = e.Packet.Robot1Health;
				_opponentHealth = e.Packet.Robot2Health;
				_sensorData = e.Packet.Robot1SensorData;

				if (e.Packet.LockSkeleton1)
					_resetSkeleton = true;
				else
					_resetSkeleton = false;
			}
			else
			{
				// if our new health is less than our current health, assume a hit occurred
				if (e.Packet.Robot2Health < _myHealth)
					AudioPlayer.PlayCue(SoundCue.Hit);

				_myHealth = e.Packet.Robot2Health;
				_opponentHealth = e.Packet.Robot1Health;
				_sensorData = e.Packet.Robot2SensorData;

				if (e.Packet.LockSkeleton2)
					_resetSkeleton = true;
				else
					_resetSkeleton = false;
			}

			SetGameState(e.Packet.GameState);
		}

		private void InitializeKinect()
		{
			if (KinectSensor.KinectSensors.Count == 0 || KinectSensor.KinectSensors[0].Status != KinectStatus.Connected)
				throw new ApplicationException("No Kinects found in Connected state!");

			 _kinect = KinectSensor.KinectSensors[0];

			_kinect.ColorFrameReady += _kinect_ColorFrameReady;
			_kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);

			var parameters = new TransformSmoothParameters
			                 	{
			                 		Smoothing = 0.3f,
			                 		Correction = 0.0f,
			                 		Prediction = 0.0f,
			                 		JitterRadius = 1.0f,
			                 		MaxDeviationRadius = 0.5f
			                 	};

			_kinect.SkeletonStream.Enable(parameters);
			_kinect.SkeletonFrameReady += _kinect_SkeletonFrameReady;
			_kinect.AudioSource.EchoCancellationMode = EchoCancellationMode.CancellationOnly;
			_kinect.AudioSource.NoiseSuppression = true;

			_kinect.Start();
		}

		private void _kinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
		{
			using (ColorImageFrame frame = e.OpenColorImageFrame())
				RingVideo.imgOpposingPlayer.Source = frame.ToBitmapSource();
		}

		private void InitializeKinectService()
		{
			if (Settings.Default.UseKinectService)
			{
				// services for others to connect to
				_colorListener = new ColorListener(_kinect, _mySettings.ColorPort, ImageFormat.Jpeg);
				_colorListener.Start();

				_skeletonListener = new SkeletonListener(_kinect, _mySettings.SkeletonPort);
				_skeletonListener.Start();

				if (!Settings.Default.MuteKinectAudio)
				{
					_audioListener = new AudioListener(_kinect, _mySettings.AudioPort);
					_audioListener.Start();
				}
			}
			else
				ConnectOpponent.IsEnabled = false;
		}

		private void ConnectToOpponentConsole()
		{
			if (Settings.Default.UseKinectService)
			{
				// services that we're connecting to
				_colorClient = new ColorClient();
				_colorClient.ColorFrameReady += _colorClient_ColorFrameReady;
				_colorClient.Connect(_opponentSettings.UserConsoleIpAddress, _opponentSettings.ColorPort);

				if (!Settings.Default.MuteKinectAudio)
				{
					_audioClient = new AudioClient();
					_audioClient.AudioFrameReady += _audioClient_AudioFrameReady;
					_audioClient.Connect(_opponentSettings.UserConsoleIpAddress, _opponentSettings.AudioPort);
				}
			}
		}

		private void ConnectToRobotKinectService()
		{
			// services that we're connecting to
			_robotColorClient = new ColorClient();
			_robotColorClient.ColorFrameReady += _robotColorClient_ColorFrameReady;
			_robotColorClient.Connect(_mySettings.RobotIpAddress, _mySettings.RobotColorPort);
		}

		private void _robotColorClient_ColorFrameReady(object sender, ColorFrameReadyEventArgs e)
		{
			// TODO: do something with the robot frame
		}

		private void _kinect_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
		{
			using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
			{
				if (skeletonFrame == null)
					return;

				if (_allSkeletons == null)
					_allSkeletons = new Skeleton[skeletonFrame.SkeletonArrayLength];

				skeletonFrame.CopySkeletonDataTo(_allSkeletons);

				if (_resetSkeleton)
				{
					bool hasSkeletons = (from s in _allSkeletons where s.TrackingState == SkeletonTrackingState.Tracked select s).Any();

					if (hasSkeletons)
					{
						double lowestValue = 10.0;

						foreach (Skeleton s in _allSkeletons)
						{
							double currentValue = SkeletonProcessor.DistanceFromRingCenter(s);

							if (currentValue < lowestValue)
							{
								lowestValue = currentValue;
								_trackingSkeletonId = s.TrackingId;
							}
						}
						_resetSkeleton = false;
					}
					else
					{
						_skeletonData = null;
					}
				}
				else
				{
					_skeletonData = (from s in _allSkeletons where s.TrackingId == _trackingSkeletonId select s).FirstOrDefault();
				}

				/*
				Skeleton first = (from skeletonData in _allSkeletons
								  where skeletonData.TrackingState == SkeletonTrackingState.Tracked
								  select skeletonData).FirstOrDefault();

				_skeletonData = first;
				*/
			}
		}

		private void _colorClient_ColorFrameReady(object sender, ColorFrameReadyEventArgs e)
		{
			// TODO: Do something with the opponent frame
		}

		private void _audioClient_AudioFrameReady(object sender, AudioFrameReadyEventArgs e)
		{
			if (MuteKinectAudio.IsChecked.HasValue && (!MuteKinectAudio.IsChecked.Value && !Settings.Default.MuteKinectAudio))
				_provider.AddSamples(e.AudioFrame.AudioData, 0, e.AudioFrame.AudioData.Length);
		}

		private void btnStartMouseDown(object sender, MouseButtonEventArgs e)
		{
			SetGameState(GameState.Reset);
		}

		private void SetGameState(GameState gs)
		{
			if (gs == GameState.Started && _gameState == GameState.GameOver)
				_gameWatch.Reset();

			if (gs == GameState.Started)
				_gameWatch.Start();

			if (gs == GameState.Paused || gs == GameState.GameOver)
				_gameWatch.Stop();

			if (gs == GameState.Reset)
			{
				_resetSkeleton = true;
				_gameWatch.Reset();
			}

			if (gs != GameState.Reset)
				_gameState = gs;
		}

		private void CompositionTarget_Rendering(object sender, EventArgs e)
		{
			ConnectedToOpponent.Text = Settings.Default.UseKinectService
			                           	? (_colorClient != null && _colorClient.IsConnected).ToString()
			                           	: "Disabled";
			ConnectedToRobot.Text = (_robotColorClient != null && _robotColorClient.IsConnected).ToString();
			GameStateDisplay.Text = _gameState.ToString();
			MyHealthDisplay.Text = _myHealth.ToString();
			OpponentHealthDisplay.Text = _opponentHealth.ToString();
			//leftPunch.Text = SkeletonProcessor.LeftPunchValues[0].ToString("0.00") + " " +
			//    SkeletonProcessor.LeftPunchValues[1].ToString("0.00") + " " +
			//    SkeletonProcessor.LeftPunchValues[2].ToString("0.00") + " " ;

			//leftWing.Text = SkeletonProcessor.LeftWingValues[0].ToString("0.00") + " " +
			//    SkeletonProcessor.LeftWingValues[1].ToString("0.00") + " " +
			//    SkeletonProcessor.LeftWingValues[2].ToString("0.00") + " ";

			//leftVel.Text = SkeletonProcessor.LeftVelocities[0].ToString("0.00") + " " +
			//    SkeletonProcessor.LeftVelocities[1].ToString("0.00") + " " +
			//    SkeletonProcessor.LeftVelocities[2].ToString("0.00") + " ";

			RingTimer.SetDuration(TimeSpan.FromMilliseconds(_gameWatch.ElapsedMilliseconds));

			// UPDATE UX COMPONENTS
			DrawArmWings();

			RingAvatar.CrunchSkeleton();
			RingAvatar.UpdateUX();

			AnimateDamage();
		}

		private void GameLoop()
		{
			while (_runGameLoop)
			{
				var packet = SkeletonProcessor.AnalyzeSkeleton(_skeletonData);

				_dataListener.Send((_gameState == GameState.Started) ? packet : new MovementDescriptorPacket());

				Dispatcher.BeginInvoke(new Action(() =>
				                                  	{
				                                  		if (packet.Move > MoveClip)
				                                  			EventController.OnMoveRobot(RobotRings.Reverse, null);

				                                  		if (packet.Move < -MoveClip)
				                                  			EventController.OnMoveRobot(RobotRings.Forward, null);

				                                  		if (packet.Strafe > MoveClip)
				                                  			EventController.OnMoveRobot(RobotRings.StrafeLeft, null);

				                                  		if (packet.Strafe < -MoveClip)
				                                  			EventController.OnMoveRobot(RobotRings.StrafeRight, null);

				                                  		if (packet.PunchLeft)
				                                  		{
				                                  			EventController.OnMoveRobot(RobotRings.PunchLeft, null);

				                                  			AudioPlayer.PlayCue(SoundCue.Punch);
				                                  			_tank0Power -= .02;
				                                  		}

				                                  		if (packet.PunchRight)
				                                  		{
				                                  			EventController.OnMoveRobot(RobotRings.PunchRight, null);

				                                  			AudioPlayer.PlayCue(SoundCue.Punch);
				                                  			_tank1Power -= .02;
				                                  		}
				                                  	}));

				Thread.Sleep(33);
			}
		}

		private void AnimateDamage()
		{
			_healthPercTarget = _myHealth/(double) Common.Settings.MaxRobotHealth;
			_healthPerc += (_healthPercTarget - _healthPerc)*.1;
			SetDamage(figure_damage, _healthPerc, 188.0, 6.28, 28.0);

			// THIS SHOULD BE BASED ON A COUNT OF HOW MANY TIME THE PUNCHES WERE THROWN
			SetDamage(figure_tank1, _tank0Power, 162.0, 2.95, 4.0);
			SetDamage(figure_tank2, _tank1Power, 162.0, 2.95, 4.0);

			imgD0.Opacity = _damageTopLeft;
			imgD1.Opacity = _damageTopRight;
			imgD2.Opacity = _damageBottomLeft;
			imgD3.Opacity = _damageBottomRight;

			if (_damageTopLeft > 0.0)
				_damageTopLeft -= .02;

			if (_damageTopRight > 0.0)
				_damageTopRight -= .02;

			if (_damageBottomLeft > 0.0)
				_damageBottomLeft -= .02;

			if (_damageBottomRight > 0.0)
				_damageBottomRight -= .02;

			if (_sensorData.IsLeftSwitchTriggered && !_lastLeft)
			{
				_damageTopLeft = .5 + _random.NextDouble()*.5;
				_damageBottomLeft = .5 + _random.NextDouble()*.5;
			}
			if (_sensorData.IsRightSwitchTriggered && !_lastRight)
			{
				_damageTopRight = .5 + _random.NextDouble()*.5;
				_damageBottomRight = .5 + _random.NextDouble()*.5;
			}

			_lastLeft = _sensorData.IsLeftSwitchTriggered;
			_lastRight = _sensorData.IsRightSwitchTriggered;
		}

		private void DrawArmWings()
		{
			for (int i = 0; i < _rightWings.Length; i++)
			{
				SetWing(_rightWings[i], SkeletonProcessor.RightPunchValues[i], _wingRadii[i], _wingMaximum[i], _wingWidth[i]);
			}

			for (int i = 0; i < _leftWings.Length; i++)
			{
				SetWing(_leftWings[i], SkeletonProcessor.LeftPunchValues[i], _wingRadii[i], _wingMaximum[i], _wingWidth[i]);
			}
		}

		private void SetWing(PathFigure blade, double angleNormal, double radius, double maxWingAngle, double bladeWidth)
		{
			double angle = angleNormal*maxWingAngle;
			var innerRadius = new Size(radius, radius);
			var outerRadius = new Size(radius + bladeWidth, radius + bladeWidth);
			var innerArcStart = new Point(radius, 0.0);
			var outerArcStart = new Point(radius + bladeWidth, 0.0);

			blade.Segments.Clear();
			blade.StartPoint = innerArcStart;
			blade.Segments.Add(new LineSegment(outerArcStart, true));

			bool isLarge = Math.Abs(angle) > 3.142;

			blade.Segments.Add(new ArcSegment(
			                   	new Point(Math.Cos(angle)*outerRadius.Height, Math.Sin(angle)*outerRadius.Height),
			                   	outerRadius, 0, isLarge, SweepDirection.Counterclockwise, true));

			blade.Segments.Add(new LineSegment(
			                   	new Point(Math.Cos(angle)*innerRadius.Width, Math.Sin(angle)*innerRadius.Width),
			                   	true));

			blade.Segments.Add(new ArcSegment(innerArcStart, innerRadius, 0, isLarge, SweepDirection.Clockwise, true));
		}

		private void SetDamage(PathFigure blade, double angleNormal, double radius, double maxWingAngle, double bladeWidth)
		{
			double angle = angleNormal*maxWingAngle;
			var innerRadius = new Size(radius, radius);
			var outerRadius = new Size(radius + bladeWidth, radius + bladeWidth);
			var innerArcStart = new Point(radius, 0.0);
			var outerArcStart = new Point(radius + bladeWidth, 0.0);

			blade.Segments.Clear();
			blade.StartPoint = innerArcStart;
			blade.Segments.Add(new LineSegment(outerArcStart, true));

			bool isLarge = Math.Abs(angle) > 3.142;

			blade.Segments.Add(new ArcSegment(
			                   	new Point(Math.Cos(angle)*outerRadius.Height, Math.Sin(angle)*outerRadius.Height),
			                   	outerRadius, 0, isLarge, SweepDirection.Clockwise, true));

			blade.Segments.Add(new LineSegment(
			                   	new Point(Math.Cos(angle)*innerRadius.Width, Math.Sin(angle)*innerRadius.Width),
			                   	true));

			blade.Segments.Add(new ArcSegment(innerArcStart, innerRadius, 0, isLarge, SweepDirection.Counterclockwise, true));
		}
	}
}